這裡是「Three.js學習日誌」的第14篇,本篇的主旨是要透過一個簡單的範例操作,來一步一步介紹如何做出更為真實的質感,這系列的文章假設讀者看得懂javascript,並且有Canvas 2D Context的相關知識。
到目前為止的系列文中,我們基本上只有使用過MeshStandardMaterial
還有MeshBasicMaterial
。所以接下來的2天我打算透過試做一個簡單的小場景,來學習材質與渲染的關係。
這次我們先從一個只有camera
的Scene
開始。
為了節省篇幅,初始化的環節也是先略過了。
const geo = new SphereGeometry(1, 100, 100);
const planeGeo = new PlaneGeometry(20, 20, 20, 20);
const mat1 = new MeshStandardMaterial({color:new Color('#eee')});
const mat2 = new MeshStandardMaterial({color:new Color('#eee')});
const mesh = new Mesh(geo, mat1);
const planeMesh = new Mesh(planeGeo, mat2);
scene.add(mesh, planeMesh);
當然這時候還是黑一片,因為沒有光源
const al = new AmbientLight(0xffffff,1)
const pl = new PointLight(0xffffff, 0.3);
pl.position.set(3, 3, 3);
scene.add(pl,al);
備註: 光源這邊原本漏掉環境光,我在2022/10/1晚上有重新修正錯誤的部分,重新補上環境光,並且下修點光源的強度。
因為PlaneGeometry
在一開始建立的時候會是直立的(而不是橫躺),所以這邊我們必須要來點旋轉。
這邊我們可以直接使用Object3D.lookAt
,這個方法其實就跟字面上的意思一樣,可以讓Object3D
物件「看」向某個目標座標。這邊我們讓平面朝向(0,1,0),也就是說面會朝上。
然後再稍微調整一下球的位置到(0,1,0),讓它剛好落在平面上(半徑是1)
planeMesh.lookAt(0,1,0);
mesh.position.set(0,1,0);
這邊我們又要來超前一下進度了。
因為現在平面是完全垂直於我們的螢幕,所以看起來像一條線。
但這樣實在太無趣,所以我們要給他來點可控性。
OrbitControl
就是環形攝影機軌道
的意思,Three.js
有提供這樣的機能讓我們可以用滑鼠直接操作camera
,讓我們可以從不同的角度去觀察我們渲染的Scene。
作法很簡單,首先我們要先建立OrbitControl
的實例,接著把camera
和renderer.domElement
當作參數傳入。
這邊因為cdn.skypack.dev
的Three.js
Package沒有提供OrbitControl
的Module,所以我們必須要引用別的Package。
正常來講
Three.js
的npm package底下是可以找到OrbitControl
的,我們之後會再提到。
import threeTsOrbitControls from 'https://cdn.skypack.dev/@three-ts/orbit-controls';
const controls = new threeTsOrbitControls.OrbitControls(camera, renderer.domElement)
接著我們得在tick loop(也就是requestAnimationFrame
的迴圈)裡面呼叫Controls.update
let time = 0;
const loop = (time) => {
mesh.rotation.y = time / 1000;
controls.update();
renderer.render(scene, camera);
requestAnimationFrame((time) => {
loop(time);
});
};
loop();
這樣就可以動手旋轉畫面了。
最後我們可以調整一下camera
的位置,讓畫面不要看起來這麼無趣。
這邊其實有個小撇步,假如我在利用OrbitControls
旋轉角度,感覺有某個角度特別中意的話,我們其實可以把camera
暴露為全域物件。
window.cm = camera;
這樣我們就可以先使用OrbitControls
把畫面轉到我們喜歡的位置,然後用開發者工具印出來camera
目前的位置,接著再把這些數據紀錄到程式裡面。
camera.position.set(0.8182387794884614,3.0688847649716244,3.8616617665282886)
這樣攝影機就會以我們中意的位置作為初始狀態來呈現了~
馬上來試玩一下~上一篇介紹的金屬化/粗糙度,這兩個屬性可以讓我們調整Material
的反射率/漫反射率。
const mat1 = new MeshStandardMaterial({
color: new Color("#eee"),
metalness: 0.5,
roughness: 0.3
});
const mat2 = new MeshStandardMaterial({
color: new Color("#eee"),
metalness: 0.2,
roughness: 0.8
});
const mesh = new Mesh(geo, mat1);
const planeMesh = new Mesh(planeGeo, mat2);
...
const al = new AmbientLight(0xffffff, 1); //補一個環境光
const pl = new PointLight(0xffffff, 0.3); //減弱點光源的強度
scene.add(al);
接著又是要來超前進度啦~
做到這邊為止雖然好像有越來越像一回事(?)
但各位大概會發現球有時感覺像是飄在空中的。
這是因為我們缺乏了陰影
在Three.js
中,如果我們想要透過現有的光源來運算物體實時的陰影,那首先我們會需要做兩件事。
光源&會造成陰影的物體必須要設定castShadow
上面會有陰影的物體必須要設定receiveShadow
所以這邊我們必須給PointLight
和球設定castShadow
,並且給平面設定receiveShadow
。
mesh.castShadow = true;
...
planeMesh.receiveShadow = true;
...
pl.castShadow = true;
設定完castShadow
/receiveShadow
之後,接著則是要打開renderer
的shadowMap
設置,並且把shadow type
改為PCFSoftShadowMap
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = PCFSoftShadowMap // PCFSoftShadowMap記得要從module import
這樣就有影子了~
但是在這邊我們可以注意到,陰影的邊緣似乎有點瑕疵。
這個問題發生的原因其實是因為燈光的shadow map
解析度不夠,所以我們要再調整一下。
pl.shadow.mapSize.width = 2048;
pl.shadow.mapSize.height = 2048;
Perfect Shadow !
到這邊我們已經做出來一個簡單的小場景,明天我們會繼續用這個範例來介紹環境貼圖和一些我們目前尚未使用過的材質。